﻿#define DEBUG


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using XDMessaging;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace PurpleKnife.Shell
{
    class Program
    {

        [STAThread]
        static void Main(string[] args)
        {
            // initialization
            try
            {
                // check for parent PID
                CheckParentPID();                
                
                // read environment variables
                ReadEnvVars();

                // setup the console window
                SetupConsoleWindow();
                
                // setup IPC
                //channelIPC = "test";
                SetupIPC();
            }
            catch (Exception ex)
            {
                AbortProgram("Failed to start shell (" + ex.Message + ").", 1);
            }
            
            // connection
            try
            {
                // SSH connection testing
                //ConnectSSH("172.16.1.0", 22, "admin", "password");
                
                // build SSH connection
                ConnectSSH(remoteSSHAddr, remoteSSHPort, remoteSSHUser, remoteSSHPass);
                
                // execute main loop
                MainLoop();
            }
            catch (Exception ex)
            {
                AbortProgram("Connection attempt failed (" + ex.Message + ").", 1);
            }
        }

        #region Private data & constants
            // read without echo
            const bool NO_ECHO = true;
            // signal program shutdown
            static bool shutdownSignal = false;
            // cursor location memory
            static private int stoCurPosX = 0;
            static private int stoCurPosY = 0;
            // environment info
            static string displayName = "unknown";
            static string remoteSSHAddr = "0.0.0.0";
            static int remoteSSHPort = 22;
            static string remoteSSHUser = "user";
            static string remoteSSHPass = "password";
            static string remoteSSHKeyFingerprint = "";
            // IPC
            static int parentPID = 0;
            // Console Colors and Text attributes
            static ConsoleColor foregroundColor = ConsoleColor.Gray;
            static ConsoleColor backgroundColor = ConsoleColor.Black;
            static bool attributeInvert = false;
            static bool attributeBright = false;
            static bool attributeHidden = false;
        #endregion

        #region Main Program Loop
            static void MainLoop()
            {
                try
                {
                    // run until abort signal or shell connection lost
                    while (shutdownSignal == false & SSHShell != null)
                    {
                        // paradigm: read before write / receive before transmit
                        // read from SSH shell stream and print to console
                        if (SSHShell.DataAvailable)
                        {
                            // transform, interpret and print shell output
                            ProcessRXData(SSHShell.Read());
                        }
                        // read from console and send to SSH shell stream
                        if (Console.KeyAvailable)
                        {
                            // transform and send console input
                            ProcessTXData(Console.ReadKey(NO_ECHO));
                        }
                    }
                    // shutdown initiated or shell disconnected
                    AbortProgram("Connection closed.", 0);
                }
                catch (Exception ex)
                {
                    AbortProgram("Exception (" + ex.Message + ") occured. Closing shell.", 1);
                }
            }
        #endregion

        #region Console Event Handling Stuff
            [System.Runtime.InteropServices.DllImport("kernel32.dll")]
            static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandlerDelegate handlerProc, bool add);
            enum ConsoleCtrlHandlerCode : uint
            {
                CTRL_C_EVENT = 0,
                CTRL_BREAK_EVENT = 1,
                CTRL_CLOSE_EVENT = 2,
                CTRL_LOGOFF_EVENT = 5,
                CTRL_SHUTDOWN_EVENT = 6
            }
            delegate bool ConsoleCtrlHandlerDelegate(ConsoleCtrlHandlerCode eventCode);
            static ConsoleCtrlHandlerDelegate _consoleHandler;
            const byte CTRL_C = 3;

            // Console Event Handler
            static bool ConsoleEventHandler(ConsoleCtrlHandlerCode eventCode)
            {
                // Handle close event here...
                switch (eventCode)
                {
                    //case ConsoleCtrlHandlerCode.CTRL_C_EVENT:
                    //    // FIXED: does not work - using Console.TreatControlCAsInput
                    //    SSHShell.WriteByte(CTRL_C);
                    //    SSHShell.Write("^C");
                    //    return (true);
                    case ConsoleCtrlHandlerCode.CTRL_BREAK_EVENT:
                        // send break
                        // TODO: not implemented
                        return (true);
                    case ConsoleCtrlHandlerCode.CTRL_CLOSE_EVENT:
                    case ConsoleCtrlHandlerCode.CTRL_LOGOFF_EVENT:
                    case ConsoleCtrlHandlerCode.CTRL_SHUTDOWN_EVENT:
                        // signal shutdown
                        shutdownSignal = true;
                        break;
                }
                return (false);
            }
            #endregion

        #region IPC
            static XDMessagingClient IPCClient = new XDMessagingClient();
            static IXDBroadcaster IPCBroadcaster;
            static IXDListener IPCListener;
            static string channelIPC = "UNDEFINED";

            static void SetupIPC()
            {
                // set up IPC
                IPCBroadcaster = IPCClient.Broadcasters.GetBroadcasterForMode(XDTransportMode.Compatibility);
                IPCListener = IPCClient.Listeners.GetListenerForMode(XDTransportMode.Compatibility);
                IPCListener.RegisterChannel(channelIPC);
                IPCListener.MessageReceived += (o, e) => IPCOnMessageReceived(o, e);
                IPCBroadcaster.SendToChannel(channelIPC, "CLI:INIT:" + System.Diagnostics.Process.GetCurrentProcess().Id);
            }

            static void IPCOnMessageReceived(object o, XDMessageEventArgs e)
            {
                string[] messageparts = e.DataGram.Message.Split('¦');
                switch (messageparts[0])
                {
                    case "SRV:SHUTDOWN":
                        // close shell immediately
                        try { SSHCli.Disconnect(); }
                        catch { ; }
                        System.Environment.Exit(0);
                        break;
                    case "SRV:DISPLAYNAME":
                        // update switch displayname
                        displayName = messageparts[1];
                        UpdateWindowTitle();
                        break;
                    case "SRV:ACTIVATE":
                    case "SRV:BLAH":
                        break;

                }

            }
        #endregion

        #region SSH Connection
            static Renci.SshNet.SshClient SSHCli;
            static Renci.SshNet.ShellStream SSHShell;

            static void SSHErrorOccured(object sender, Renci.SshNet.Common.ExceptionEventArgs e)
            {
                Debug.Print("*********** ERROR: " + e.Exception.Message);
                shutdownSignal = true;
            }

            static void ConnectSSH(string IPAddress, int Port, string Username, string Password)
            {
                SSHCli = new Renci.SshNet.SshClient(IPAddress, Port, Username, Password);
                SSHCli.KeepAliveInterval = TimeSpan.FromSeconds(90);
                SSHCli.ConnectionInfo.Timeout = TimeSpan.FromSeconds(5);
                SSHCli.ErrorOccurred += new EventHandler<Renci.SshNet.Common.ExceptionEventArgs>(SSHErrorOccured);
                SSHCli.HostKeyReceived += new EventHandler<Renci.SshNet.Common.HostKeyEventArgs>(sshCli_HostKeyReceived);
                try
                {
                    SSHCli.Connect();
                    SSHShell = SSHCli.CreateShellStream("ansi", (uint)Console.WindowWidth, (uint)Console.WindowHeight, 800, 600, (Console.BufferWidth * Console.BufferHeight));
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Exception (" + ex.Message + ") occured while connecting to SSH server.");
                }
            }

            static void sshCli_HostKeyReceived(object sender, Renci.SshNet.Common.HostKeyEventArgs e)
            {
                System.Diagnostics.Debug.Print("SSH Remote Host: " + SSHCli.ConnectionInfo.Host);
                System.Diagnostics.Debug.Print("SSH Key Fingerprint: " + BitConverter.ToString(e.FingerPrint));
            }
        
        #endregion

        #region I/O Processing

            static void ProcessTXData(ConsoleKeyInfo ConKey)
            {
                // transform keyboard input
                //Debug.Print("****** Keypress: " + (int)ConKey.Key);
                switch ((int)ConKey.Key)
                {
                    case 8:
                        // send backspace
                        SSHShell.Write(((char)127).ToString());
                        break;
                    case 37:
                        // cursor left
                        SSHShell.Write(((char)27).ToString());
                        SSHShell.Write("[D");
                        break;
                    case 38:
                        // cursor up
                        SSHShell.Write(((char)27).ToString());
                        SSHShell.Write("[A");
                        break;
                    case 39:
                        // cursor right
                        SSHShell.Write(((char)27).ToString());
                        SSHShell.Write("[C");
                        break;
                    case 40:
                        // cursor down
                        SSHShell.Write(((char)27).ToString());
                        SSHShell.Write("[B");
                        break;
                    default:
                        SSHShell.Write(ConKey.KeyChar.ToString());
                        break;
                }            
                //Debug.Print("*****************Sending Keychar: " + (int)ConKey.KeyChar);
            }

            // ANSI (VT100) escape sequence decoder and shell output transform
            static void ProcessRXData(string Data)
            {
                char curChar;
                string escStr = "";
                char lastCharInEscStr = (char)0;
                bool isEscaped = false;

                for (int i = 0; i < Data.Length; i++)
                {
                    curChar = Data[i];
                    if (isEscaped)
                    {
                        escStr = escStr + curChar.ToString();
                        if (escStr.Length > 0) { lastCharInEscStr = escStr[escStr.Length - 1]; }
                        if (Char.IsLetter(lastCharInEscStr))
                        {
                            // ESC sequence is complete
                            isEscaped = false;
                            Debug.WriteLine("***************ESC SEQ: " + escStr);
                            PrintANSI(escStr);
                        }
                    }
                    else
                    {
                        //Debug.WriteLine("***********Received Char: " + (int)curChar);
                        switch ((int)curChar)
                        {
                            case 7:
                                // ignore bell
                                break;
                            case 8:
                                // backspace (overwrite with NULL)
                                Console.Write((char)8);
                                Console.Write((char)0);
                                Console.Write((char)8);
                                break;
                            case 13:
                                // carriage return - move cursor to column 0
                                Console.CursorLeft = 0;
                                break;
                            case 27:
                                // escape
                                isEscaped = true;
                                escStr = "";
                                break;
                            default:
                                // filter /0 chars and print the rest
                                if (curChar != (char)0) { Console.Write(curChar); }
                                break;
                        }


                    }
                }
            }

        #endregion

        # region ANSI Interpreter

            // ANSI escape sequence interpreter (still incomplete)
            static void PrintANSI(string EscapeString)
            {
                string posIndicators = "";
                char lastChar = GetLastChar(EscapeString);
                //Debug.Print("*********** Curspor Position: " + Console.CursorTop + "; " + Console.CursorLeft);
                switch (lastChar.ToString())
                {
                    case "H":
                    case "f":
                        // move cursor to line #;#
                        posIndicators = EscapeString.Remove(0, 1);
                        posIndicators = posIndicators.Remove(posIndicators.Length - 1);
                        if ((posIndicators == "") || (posIndicators == ";"))
                        {
                            // no parameters given, use top left as default
                            Console.SetCursorPosition(0, 0);
                        }
                        else
                        {
                            // interpret row/col parameters and move cursor there
                            string[] ind = posIndicators.Split(';');
                            int newPosY = int.Parse(ind[0]) - 1;
                            int newPosX = int.Parse(ind[1]) - 1;
                            Console.SetCursorPosition(newPosX, newPosY);
                        }
                        break;
                    case "A":
                        // move cursor up # lines
                        posIndicators = EscapeString.Remove(0, 1);
                        posIndicators = posIndicators.Remove(posIndicators.Length - 1);
                        MoveCursorUp(GetMovement(posIndicators));
                        break;
                    case "B":
                        // move cursor down # lines
                        posIndicators = EscapeString.Remove(0, 1);
                        posIndicators = posIndicators.Remove(posIndicators.Length - 1);
                        MoveCursorDown(GetMovement(posIndicators));
                        break;
                    case "C":
                        // move cursor right # spaces
                        posIndicators = EscapeString.Remove(0, 1);
                        posIndicators = posIndicators.Remove(posIndicators.Length - 1);
                        MoveCursorRight(GetMovement(posIndicators));
                        break;
                    case "D":
                        // move cursor left # spaces
                        posIndicators = EscapeString.Remove(0, 1);
                        posIndicators = posIndicators.Remove(posIndicators.Length - 1);
                        MoveCursorLeft(GetMovement(posIndicators));
                        break;
                    case "R":
                        // report current line & column
                        // TODO: not implemented
                        break;
                    case "c":
                        // reset terminal device
                        ResetColors();
                        Console.SetCursorPosition(0, 0);
                        break;
                    case "s":
                    case "7":
                        // save cursor position for recall
                        stoCurPosX = Console.CursorLeft;
                        stoCurPosY = Console.CursorTop;
                        break;
                    case "u":
                    case "8":
                        // recall cursor position
                        Console.SetCursorPosition(stoCurPosX, stoCurPosY);
                        break;
                    case "J":
                        // erase screen
                        switch (EscapeString.Replace("[", ""))
                        {
                            //case "J":
                            //    // erase down
                            //    // TODO: not implemented
                            //    break;
                            case "1J":
                                // erase up
                                // TODO: not implemented
                                break;
                            case "J":
                            case "0J":
                            case "2J":
                                // erase screen
                                Console.Clear();
                                Console.SetCursorPosition(0, 0);
                                break;
                        }
                        break;
                    case "K":
                        // erase line
                        switch (EscapeString.Replace("[", ""))
                        {
                            case "1K":
                                // erase start of line
                                // TODO: not implemented
                                break;
                            case "2K":
                                // erase Line
                                // TODO: not implemented
                                break;
                            default:
                                // clear to end of line
                                // store current cursor column and line
                                int curCol = Console.CursorLeft;
                                int curLin = Console.CursorTop;
                                // overwrite with NULL to end of line
                                int spacesTillEnd = Console.BufferWidth - Console.CursorLeft - 1;
                                for (int i = 0; i < spacesTillEnd; i++) { Console.Write((char)0); }
                                // restore cursor column and line
                                Console.CursorLeft = curCol;
                                Console.CursorTop = curLin;
                                break;
                        }
                        break;
                    case "m":
                        // fix missing parameters for parsing
                        if (EscapeString == "[m") { EscapeString = "[0;0m"; }                        
                        // display attributes and colors
                        EscapeString = EscapeString.Remove(0, 1);
                        EscapeString = EscapeString.Remove(EscapeString.Length - 1);
                        if (EscapeString != "")
                        {
                            // interpret attributes
                            string[] seq = EscapeString.Split(';');
                            int val = 0;
                            for (int i = 0; i < seq.Length; i++)
                            {
                                val = int.Parse(seq[i]);
                                switch (val)
                                {
                                    // text attributes
                                    case 0:
                                        // reset colors
                                        ResetColors();
                                        break;
                                    case 1:
                                        // bright
                                        attributeBright = true;
                                        break;
                                    case 2:
                                        // dim
                                        attributeBright = false;
                                        break;
                                    case 4:
                                        // underscore not implementable
                                        break;
                                    case 5:
                                        // blink not implementable
                                        break;
                                    case 7:
                                        // reverse
                                        attributeInvert = true;
                                        break;
                                    case 8:
                                        // hidden
                                        attributeHidden = true;
                                        break;
                                    // foreground colors
                                    case 30:
                                        if (attributeBright) { foregroundColor = ConsoleColor.DarkGray; }
                                        else { foregroundColor = ConsoleColor.Black; }
                                        break;
                                    case 31:
                                        if (attributeBright) { foregroundColor = ConsoleColor.Red; }
                                        else { foregroundColor = ConsoleColor.DarkRed; }
                                        break;
                                    case 32:
                                        if (attributeBright) { foregroundColor = ConsoleColor.Green; }
                                        else { foregroundColor = ConsoleColor.DarkGreen; }
                                        break;
                                    case 33:
                                        if (attributeBright) { foregroundColor = ConsoleColor.Yellow; }
                                        else { foregroundColor = ConsoleColor.DarkYellow; }
                                        break;
                                    case 34:
                                        if (attributeBright) { foregroundColor = ConsoleColor.Blue; }
                                        else { foregroundColor = ConsoleColor.DarkBlue; }
                                        break;
                                    case 35:
                                        if (attributeBright) { foregroundColor = ConsoleColor.Magenta; }
                                        else { foregroundColor = ConsoleColor.DarkMagenta; }
                                        break;
                                    case 36:
                                        if (attributeBright) { foregroundColor = ConsoleColor.Cyan; }
                                        else { foregroundColor = ConsoleColor.DarkCyan; }
                                        break;
                                    case 37:
                                        if (attributeBright) { foregroundColor = ConsoleColor.White; }
                                        else { foregroundColor = ConsoleColor.Gray; }
                                        break;
                                    // background colors
                                    case 40:
                                        if (attributeBright) { backgroundColor = ConsoleColor.DarkGray; }
                                        else { backgroundColor = ConsoleColor.Black; }
                                        break;
                                    case 41:
                                        if (attributeBright) { backgroundColor = ConsoleColor.Red; }
                                        else { backgroundColor = ConsoleColor.DarkRed; }
                                        break;
                                    case 42:
                                        if (attributeBright) { backgroundColor = ConsoleColor.Green; }
                                        else { backgroundColor = ConsoleColor.DarkGreen; }
                                        break;
                                    case 43:
                                        if (attributeBright) { backgroundColor = ConsoleColor.Yellow; }
                                        else { backgroundColor = ConsoleColor.DarkYellow; }
                                        break;
                                    case 44:
                                        if (attributeBright) { backgroundColor = ConsoleColor.Blue; }
                                        else { backgroundColor = ConsoleColor.DarkBlue; }
                                        break;
                                    case 45:
                                        if (attributeBright) { backgroundColor = ConsoleColor.Magenta; }
                                        else { backgroundColor = ConsoleColor.DarkMagenta; }
                                        break;
                                    case 46:
                                        if (attributeBright) { backgroundColor = ConsoleColor.Cyan; }
                                        else { backgroundColor = ConsoleColor.DarkCyan; }
                                        break;
                                    case 47:
                                        if (attributeBright) { backgroundColor = ConsoleColor.White; }
                                        else { backgroundColor = ConsoleColor.Gray; }
                                        break;
                                }
                            }
                            // check for hidden / reverse and set colors
                            if (attributeHidden)
                            {
                                // hidden: foreground is background color
                                Console.BackgroundColor = backgroundColor;
                                Console.ForegroundColor = backgroundColor;
                            }
                            else
                            {
                                if (attributeInvert)
                                {
                                    // set inverted colors
                                    Console.BackgroundColor = foregroundColor;
                                    Console.ForegroundColor = backgroundColor;
                                }
                                else
                                {
                                    // set straight colors
                                    Console.BackgroundColor = backgroundColor;
                                    Console.ForegroundColor = foregroundColor;
                                }
                            }
                        }
                        break;
                    case "h":
                        // screen mode
                        break;
                    default:
                        Debug.Print("********** unrecognized ANSI ESC sequence: " + EscapeString);
                        break;

                }
            }

            static void ResetColors()
            {
                Console.ResetColor();
                attributeBright = false;
                attributeInvert = false;
                attributeHidden = false;
                foregroundColor = Console.ForegroundColor;
                backgroundColor = Console.BackgroundColor;
            }

            static void MoveCursorLeft(int Amount)
            {
                int curCol = Console.CursorLeft;
                int curLin = Console.CursorTop;
                curCol = curCol - Amount;
                // result is negative, clamp to zero
                if (curCol < 0) { curCol = 0;} 
                Console.SetCursorPosition(curCol, curLin);
            }

            static void MoveCursorRight(int Amount)
            {
                int curCol = Console.CursorLeft;
                int curLin = Console.CursorTop;
                curCol = curCol + Amount;
                Console.SetCursorPosition(curCol, curLin);            
            }

            static void MoveCursorUp(int Amount)
            {
                int curCol = Console.CursorLeft;
                int curLin = Console.CursorTop;
                curLin = curLin - Amount;
                Console.SetCursorPosition(curCol, curLin);            
            }

            static void MoveCursorDown(int Amount)
            {
                int curCol = Console.CursorLeft;
                int curLin = Console.CursorTop;
                curLin = curLin + Amount;
                Console.SetCursorPosition(curCol, curLin);
            }
        #endregion

        #region Subroutines and Helper Functions

            // setup console window properties
            static void SetupConsoleWindow()
            {

                UpdateWindowTitle();
                Console.TreatControlCAsInput = true;
                Console.SetWindowSize(120, 25);
                Console.SetBufferSize(Console.LargestWindowWidth, 5000);
                Console.SetWindowPosition(0, 0);
                // get default colors
                foregroundColor = Console.ForegroundColor;
                backgroundColor = Console.BackgroundColor;
                attributeInvert = false;
                attributeBright = false;
                attributeHidden = false;
                // set up console event handling
                _consoleHandler = new ConsoleCtrlHandlerDelegate(ConsoleEventHandler);
                SetConsoleCtrlHandler(_consoleHandler, true);
            }

            static void UpdateWindowTitle()
            {
                Console.Title = displayName + " (" + remoteSSHUser + "@" + remoteSSHAddr + ":" + remoteSSHPort + ") - PurpleKnife Shell";
            }

            // read environment variables
            static void ReadEnvVars()
            {
                // read settings from env vars
                displayName = Environment.GetEnvironmentVariable("PK_SWITCH_DISPLAYNAME");
                channelIPC = Environment.GetEnvironmentVariable("PK_IPC_CHANNEL");
                remoteSSHAddr = Environment.GetEnvironmentVariable("PK_SSH_ADDR");
                int.TryParse(Environment.GetEnvironmentVariable("PK_SSH_PORT"), out remoteSSHPort);
                remoteSSHUser = Environment.GetEnvironmentVariable("PK_SSH_USER");
                remoteSSHPass = Environment.GetEnvironmentVariable("PK_SSH_PASS");
                remoteSSHKeyFingerprint = Environment.GetEnvironmentVariable("PK_SSH_KEY_FINGERPRINT");
            }


            // check for parent PID
            static void CheckParentPID()
            {
                if (Environment.GetEnvironmentVariable("PK_PARENT_PID") != null)
                {
                    int ParentPID = int.Parse(Environment.GetEnvironmentVariable("PK_PARENT_PID"));
                }
                else
                {
                    Console.WriteLine("ERROR: Can't run PurpleKnife.Shell as a standalone program.");
                    System.Environment.Exit(1);
                }
            }

            // Abort program with message and exit code
            static void AbortProgram(string Message, int ExitCode)
            {
                GC.Collect();
                Console.WriteLine();
                Console.Write(Message.Replace("\r\n", ""));
                ConsoleExtensions.FlashWindow(5);
                Console.ReadKey();
                System.Environment.Exit(ExitCode);
            }

            // return last char in a string
            static char GetLastChar(string InString)
            {
                if (InString.Length > 0)
                {
                    return InString[InString.Length - 1];
                }
                else
                {
                    return (char)0;
                }
            }

            // return movement factor (default 1)
            static int GetMovement(string Indicator)
            {
                // get rid of single semicolon without values
                Indicator = Indicator.Replace(";", "");
                // check for missing factor
                if (Indicator == "")
                {
                    // return default
                    return 1;
                }
                else
                {
                    // return movement factor
                    return int.Parse(Indicator);
                }
            }
        #endregion

    }
}
